package com.webmining.sherlock.crawler;

import com.webmining.sherlock.admin.Configuration;
import com.webmining.sherlock.admin.ConfigurationException;
import org.apache.log4j.Logger;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public class Scope {

	protected Logger log;
	
	public static final String RULE_TYPE_DOMAIN = "DOMAIN";
	public static final String RULE_TYPE_IP = "IP";

	public static final String RULE_ACTION_ACCEPT = "ACCEPT";
	public static final String RULE_ACTION_DROP = "DROP";
	
	public class Rule {
		public String type;
		public Pattern pattern;
		public String action;
		public String toString() {
			return "[type=" + type + ", pattern=" + pattern.pattern() + ", action=" + action + "]";
		}
	}
	
    protected ArrayList rules;

    public Scope() throws CrawlingException {
    	log = Logger.getLogger(Scope.class);
        rules = new ArrayList();
        loadRules();
    }

    /**
     * Analize an string rule and stores in the scope
     * @param rule Rule in string format (i.e. "DOMAIN *.gentoo.org ACCEPT")
     */
    public void addRule(String rule) throws CrawlingException {
    	log.debug("Adding rule " + rule);
    	StringTokenizer token = new StringTokenizer(rule);
    	Rule internalRule = new Rule();
    	if (token.hasMoreTokens()) {
    		internalRule.type = token.nextToken().toUpperCase();
    		if (!internalRule.type.equals(RULE_TYPE_DOMAIN) &&
    				!internalRule.type.equals(RULE_TYPE_IP)) {
    			throw new CrawlingException("Invalid rule type " +
    					internalRule.type);
    		}
    	} else {
    		log.error("Unable to get the rule type for " +
    				"rule " + rule);
    		throw new CrawlingException("Unable to get the rule type for " +
    				"rule " + rule);
    	}
	if (token.hasMoreTokens()) {
		try {
			internalRule.pattern = Pattern.compile(token.nextToken());
		} catch (PatternSyntaxException pse) {
			log.error("Invalid pattern syntax for rule " + rule, pse);
			throw new CrawlingException("Invalid pattern syntax " +
					"for rule " + rule, pse);
		}
	} else {
		log.error("Unable to get the rule pattern " +
				"for rule " + rule);
		throw new CrawlingException("Unable to get the rule pattern " +
				"for rule " + rule);
	}
	if (token.hasMoreTokens()) {
		internalRule.action = token.nextToken().toUpperCase();
		if (!internalRule.action.equals(RULE_ACTION_ACCEPT) &&
				!internalRule.action.equals(RULE_ACTION_DROP)) {
			log.error("Invalid rule action " +
					internalRule.action);
			throw new CrawlingException("Invalid rule action " +
					internalRule.action);
		}
		
	} else {
		log.error("Unable to get the rule action " +
				"for rule " + rule);
		throw new CrawlingException("Unable to get the rule action " +
				"for rule " + rule);
	}
   	rules.add(internalRule);
    }
    
    public void acceptDomain(String domain) {
    	Rule rule = new Rule();
    	rule.type = "DOMAIN";
    	rule.pattern = domainToPattern(domain);
    	rule.action = "ACCEPT";
    	rules.add(rule);
    }
    
    public static Pattern domainToPattern(String domain) {
    	Logger log = Logger.getLogger(Scope.class);
    	log.debug("Creating pattern rule for domain " + domain);
    	String patternString = domain.replaceAll("\\.", "\\\\.");
    	return Pattern.compile(".*" + patternString);
    }

    /**
     * Looks up if a certain host is in the crawling scope. The scope rules are
     * used in a drop down direction, so if an early rule accept a host a later
     * rule can dropped so the final check status for that host will be not in
     * the scope.
     */
    public boolean inScope(String host) {
        boolean accepted = false;
        Iterator internalRules = rules.iterator();
        while (internalRules.hasNext()) {
        	Rule rule = (Rule) internalRules.next();
        	Matcher matcher = rule.pattern.matcher(host);
        	if (matcher.find()) {
        		if (rule.action.equals(RULE_ACTION_ACCEPT)) {
        			accepted = true;
        			log.debug("potentially accepting host " + host + " by rule " + rule);
        		} else {
        			accepted = false;
        			log.debug("potentially dropping host " + host + " by rule " + rule);
        		}
        	}
        }
        if (accepted) {
        	log.debug("host " + host + " finally accepted");
        } else {
        	log.debug("host " + host + " finally dropped");
        }
        return accepted;
    }
    
    public void loadRules() throws CrawlingException {
    	String scopeFilename = null;
    	try {
    		scopeFilename= Configuration.getScopeConfigFilename();
        } catch(ConfigurationException ce) {
			log.error("Unable to get the scope filename", ce);
			throw new CrawlingException("Unable to get the scope filename", ce);
        }
        try {
            BufferedReader scopeFile =
                    new BufferedReader(new InputStreamReader(
                    Scope.class.getClassLoader().getResourceAsStream(scopeFilename)));
            String ruleLine;
            while ((ruleLine = scopeFile.readLine()) != null) {
            	// Clean the rule line
            	ruleLine = ruleLine.trim();
            	// If the rule line is not empty and is not a comment add it
            	// to the internal rules
            	if (ruleLine.length() > 0 && ruleLine.charAt(0) != '#') {
            		this.addRule(ruleLine);
            	}
            }
        } catch(FileNotFoundException fnfe) {
        	String msg = "The scope file " + scopeFilename + " was not found ";
        	log.error(msg, fnfe);
            throw new CrawlingException(msg, fnfe);
        } catch(IOException ioe) {
        	String msg = "Error reading the scope file " + scopeFilename;
        	log.error(msg, ioe);
            throw new CrawlingException(msg, ioe);
        }
    }
    
}
